Skip to content

poc: add schema command to fetch Clerk API specs#37

Draft
kylemac wants to merge 3 commits intomainfrom
kylemac/add-openapi-command
Draft

poc: add schema command to fetch Clerk API specs#37
kylemac wants to merge 3 commits intomainfrom
kylemac/add-openapi-command

Conversation

@kylemac
Copy link
Copy Markdown
Contributor

@kylemac kylemac commented Mar 16, 2026

Summary

  • Adds a clerk schema command that fetches OpenAPI specs from the clerk/openapi-specs repo
  • Supports path-based introspection to extract specific endpoints or schema types instead of dumping the full spec
  • Supports --resolve-refs to inline all $ref references for self-contained output

Usage

clerk schema backend                          # Full Backend API spec (YAML)
clerk schema backend /users                   # Just the /users endpoint
clerk schema backend User                     # Just the User schema type
clerk schema backend /users --resolve-refs    # Endpoint with all $refs inlined
clerk schema backend User --resolve-refs      # Type with all $refs inlined
clerk schema backend --format json            # Full spec as JSON
clerk schema backend --spec-version 2024-10-01  # Specific version

Changes

  • New clerk schema command with public API names (backend, frontend, platform, webhooks) and internal aliases (bapi, fapi)
  • Path introspection: drill into specific endpoints (/users) or types (User) with prefix-aware matching and "did you mean?" suggestions
  • --resolve-refs flag: recursively inlines $ref references with circular reference detection
  • 24-hour local caching to reduce network requests
  • 36 tests covering aliases, path/type lookup, ref resolution, circular refs, format output, and error handling

Test plan

  • bun test — all 398 tests pass
  • bun run lint / bun run format — clean
  • Smoke tested clerk schema backend /users, clerk schema backend User, and --resolve-refs against live specs

🤖 Generated with Claude Code

kylemac and others added 3 commits March 16, 2026 15:23
Adds a new `clerk openapi` command that fetches OpenAPI specifications from the clerk/openapi-specs repository. Supports four public API names with options for version selection, format (YAML/JSON), and file output.

Changes:
- New command with public names: backend, frontend, platform, webhooks
- Aliases for internal names: bapi→backend, fapi→frontend
- 24-hour local caching to reduce network requests
- Comprehensive test suite with 15 test cases covering aliases, versions, formats, error handling, and file output
- Updated CLAUDE.md to require tests for new functionality

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Rename the CLI command from `clerk openapi` to `clerk schema`. Use
public-facing names (backend, frontend, platform, webhooks) as primary
identifiers while keeping internal aliases (bapi, fapi) working.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Support drilling into specific endpoints (e.g. `clerk schema backend /users`)
or schema types (e.g. `clerk schema backend User`) instead of dumping the
full spec. Add --resolve-refs flag to inline $ref references for
self-contained output with circular reference detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kylemac kylemac changed the title feat: add openapi command to fetch Clerk API specs feat: add schema command with path introspection and ref resolution Mar 17, 2026
@kylemac kylemac changed the title feat: add schema command with path introspection and ref resolution poc: add openapi command to fetch Clerk API specs Mar 17, 2026
@kylemac kylemac changed the title poc: add openapi command to fetch Clerk API specs poc: add schema command to fetch Clerk API specs Mar 19, 2026
Comment thread src/cli-program.ts
.argument("[api]", "API name: backend, frontend, platform, or webhooks")
.argument("[path]", "Endpoint path (e.g. /users) or schema type (e.g. User)")
.option("--spec-version <version>", "Spec version (default: latest)")
.option("--format <format>", "Output format: yaml (default) or json")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to have: you could use .choices(["yaml", "json"]) here instead of the manual validation in schema(). Commander would reject invalid values automatically and display the allowed values in --help output. Same idea for the [api] argument -- .choices(["backend", "frontend", "platform", "webhooks", "bapi", "fapi"]) would give you free validation and help text.

.argument("[api]", "API name").choices(["backend", "frontend", "platform", "webhooks", "bapi", "fapi"])
// ...
.option("--format <format>", "Output format").choices(["yaml", "json"])

if (outputPath) {
await Bun.write(outputPath, content + "\n");
console.error(`Spec written to ${outputPath}`);
} else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a --no-cache or --refresh flag. Right now there's no escape hatch if the cached file gets corrupted or the user needs a fresh copy within the 24h TTL window. Something like:

// in SchemaOptions
noCache?: boolean;

// in fetchSpec
if (!options.noCache) {
  const cached = await readCache(api, version);
  if (cached) return cached;
}

Comment thread src/lib/constants.ts
latest: string;
versions: string[];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version list will go stale whenever a new API version is released. You might want to add a comment noting that, or consider fetching the directory listing from the GitHub API at some point (e.g. GET repos/clerk/openapi-specs/contents/bapi) so the CLI can discover versions dynamically. For now, a TODO and maybe a simple test that fetches the repo to flag drift would be a low-cost safety net.

// TODO: consider discovering versions dynamically from the clerk/openapi-specs repo


// ── Ref resolution ───────────────────────────────────────────────────────────

export function resolveAllRefs(node: unknown, root: unknown, seen?: Set<string>): unknown {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visited.delete(refPath) pattern here is correct -- it tracks the current ancestor chain so sibling refs to the same schema both get resolved. Just wanted to confirm this is intentional since it's the kind of thing that looks like it might be a bug at first glance. Maybe a short comment would help future readers:

// Remove from visited so sibling references to the same schema
// are resolved (only circular *ancestor* chains are blocked).
visited.delete(refPath);

Copy link
Copy Markdown
Contributor

@rafa-thayto rafa-thayto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! Nice feature, this would be really useful for agents and developers introspecting the API. A few things to address:

1. File paths are under src/ instead of packages/cli-core/src/

This branch looks like it predates the monorepo restructuring. Current main has all CLI source under packages/cli-core/src/. A rebase + move would be needed before this can merge.

2. Uses console.log/console.error directly

The project has a strict no-console oxlint rule and uses log.* methods everywhere (see .claude/rules/logging.md). This will fail the lint check.

import { log } from "../../lib/log.ts";

// For pipeable data output:
log.data(content);

// For status messages:
log.info(`Spec written to ${outputPath}`);

3. Tests delete the real CLERK_CACHE_DIR

beforeEach does await rm(CLERK_CACHE_DIR, { recursive: true, force: true }) which resolves to the developer's actual cache directory. Running tests locally would wipe your real CLI cache.

// Use a temp directory instead:
const testCacheDir = await mkdtemp(join(tmpdir(), "clerk-schema-test-"));
process.env.CLERK_CONFIG_DIR = testCacheDir;

4. Cache TTL mismatch

The README says "cached locally for 24 hours" but the code uses CACHE_TTL_MS which is 1 hour in constants.ts. Either update the docs or define a separate SCHEMA_CACHE_TTL_MS = 24 * 60 * 60 * 1000.

5. Tests should use captureLog() instead of spyOn(console)

The project has a captureLog() test utility that integrates with the log.* system. Once you switch from console.log to log.*, the tests should use captureLog() too.

6. Missing changeset

The Enforce Changeset workflow will block merge. This needs a minor changeset since it adds a new user-facing command.

7. resolveAllRefs cycle detection could be stricter

The visited set is shared across sibling properties with add/delete. For diamond-shaped ref patterns, cloning the set when entering a new ref would be safer:

visited.add(refPath);
const resolved = resolveAllRefs(target, root, new Set(visited));
visited.delete(refPath);

The core implementation is really well done though. The path introspection, type lookup, "did you mean?" suggestions, and circular ref handling are all solid. Just needs the structural updates to land on current main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants